route.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { getServerSideConfig } from "@/app/config/server";
  2. import {
  3. ANTHROPIC_BASE_URL,
  4. Anthropic,
  5. ApiPath,
  6. DEFAULT_MODELS,
  7. ModelProvider,
  8. } from "@/app/constant";
  9. import { prettyObject } from "@/app/utils/format";
  10. import { NextRequest, NextResponse } from "next/server";
  11. import { auth } from "../../auth";
  12. import { collectModelTable } from "@/app/utils/model";
  13. const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
  14. async function handle(
  15. req: NextRequest,
  16. { params }: { params: { path: string[] } },
  17. ) {
  18. console.log("[Anthropic Route] params ", params);
  19. if (req.method === "OPTIONS") {
  20. return NextResponse.json({ body: "OK" }, { status: 200 });
  21. }
  22. const subpath = params.path.join("/");
  23. if (!ALLOWD_PATH.has(subpath)) {
  24. console.log("[Anthropic Route] forbidden path ", subpath);
  25. return NextResponse.json(
  26. {
  27. error: true,
  28. msg: "you are not allowed to request " + subpath,
  29. },
  30. {
  31. status: 403,
  32. },
  33. );
  34. }
  35. const authResult = auth(req, ModelProvider.Claude);
  36. if (authResult.error) {
  37. return NextResponse.json(authResult, {
  38. status: 401,
  39. });
  40. }
  41. try {
  42. const response = await request(req);
  43. return response;
  44. } catch (e) {
  45. console.error("[Anthropic] ", e);
  46. return NextResponse.json(prettyObject(e));
  47. }
  48. }
  49. export const GET = handle;
  50. export const POST = handle;
  51. export const runtime = "edge";
  52. export const preferredRegion = [
  53. "arn1",
  54. "bom1",
  55. "cdg1",
  56. "cle1",
  57. "cpt1",
  58. "dub1",
  59. "fra1",
  60. "gru1",
  61. "hnd1",
  62. "iad1",
  63. "icn1",
  64. "kix1",
  65. "lhr1",
  66. "pdx1",
  67. "sfo1",
  68. "sin1",
  69. "syd1",
  70. ];
  71. const serverConfig = getServerSideConfig();
  72. async function request(req: NextRequest) {
  73. const controller = new AbortController();
  74. let authHeaderName = "x-api-key";
  75. let authValue =
  76. req.headers.get(authHeaderName) ||
  77. req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
  78. serverConfig.anthropicApiKey ||
  79. "";
  80. let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
  81. let baseUrl =
  82. serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
  83. if (!baseUrl.startsWith("http")) {
  84. baseUrl = `https://${baseUrl}`;
  85. }
  86. if (baseUrl.endsWith("/")) {
  87. baseUrl = baseUrl.slice(0, -1);
  88. }
  89. console.log("[Proxy] ", path);
  90. console.log("[Base Url]", baseUrl);
  91. const timeoutId = setTimeout(
  92. () => {
  93. controller.abort();
  94. },
  95. 10 * 60 * 1000,
  96. );
  97. const fetchUrl = `${baseUrl}${path}`;
  98. const fetchOptions: RequestInit = {
  99. headers: {
  100. "Content-Type": "application/json",
  101. "Cache-Control": "no-store",
  102. [authHeaderName]: authValue,
  103. "anthropic-version":
  104. req.headers.get("anthropic-version") ||
  105. serverConfig.anthropicApiVersion ||
  106. Anthropic.Vision,
  107. },
  108. method: req.method,
  109. body: req.body,
  110. redirect: "manual",
  111. // @ts-ignore
  112. duplex: "half",
  113. signal: controller.signal,
  114. };
  115. // #1815 try to refuse some request to some models
  116. if (serverConfig.customModels && req.body) {
  117. try {
  118. const modelTable = collectModelTable(
  119. DEFAULT_MODELS,
  120. serverConfig.customModels,
  121. );
  122. const clonedBody = await req.text();
  123. fetchOptions.body = clonedBody;
  124. const jsonBody = JSON.parse(clonedBody) as { model?: string };
  125. // not undefined and is false
  126. if (modelTable[jsonBody?.model ?? ""].available === false) {
  127. return NextResponse.json(
  128. {
  129. error: true,
  130. message: `you are not allowed to use ${jsonBody?.model} model`,
  131. },
  132. {
  133. status: 403,
  134. },
  135. );
  136. }
  137. } catch (e) {
  138. console.error(`[Anthropic] filter`, e);
  139. }
  140. }
  141. console.log("[Anthropic request]", fetchOptions.headers, req.method);
  142. try {
  143. const res = await fetch(fetchUrl, fetchOptions);
  144. console.log(
  145. "[Anthropic response]",
  146. res.status,
  147. " ",
  148. res.headers,
  149. res.url,
  150. );
  151. // to prevent browser prompt for credentials
  152. const newHeaders = new Headers(res.headers);
  153. newHeaders.delete("www-authenticate");
  154. // to disable nginx buffering
  155. newHeaders.set("X-Accel-Buffering", "no");
  156. return new Response(res.body, {
  157. status: res.status,
  158. statusText: res.statusText,
  159. headers: newHeaders,
  160. });
  161. } finally {
  162. clearTimeout(timeoutId);
  163. }
  164. }